/**
* the Routes application event bus -- an MVC-enabling non-framework 
*/
package routes {
	
	import flash.events.Event;
	import flash.events.EventDispatcher;
	
	import mx.core.Application;
	import mx.managers.SystemManager;
	import mx.utils.ObjectUtil;
	
	/**
	 * The routes.Routes class is a generic application event bus enabling 
	 * loosely coupled applications to communicate internally entirely via 
	 * events.
	 * 
	 * <p>Routes makes no MVC assumptions or any impositions on how an 
	 * application is organized or broken into components -- each application 
	 * is free to slice and dice itself as is best for its specific case. 
	 * Niether are there any Inversion of Control impositions -- it is up to 
	 * each View to update itself internally as it knows best, all it needs 
	 * is to be alerted when affecting changes occur elsewhere in the system.</p>
	 * 
	 * <p>An application may define any number of <em>routes</em> for any event type.
	 * An event route defines a <b>logical connection</b> between an event 
	 * type and a <b>handler descriptor</b>. Multiple routes may be specified 
	 * for any single event type -- each route is processed in sequence, by 
	 * dynamically resolving the targeted component+handler and invoking it. 
	 * This also means the component registered under a name may be changed 
	 * at any time e.g. a name such as <code>confirmationPopup</code> may be 
	 * reset any number of times to the confirmation popup that is active at
	 * any given time.</p> 
	 * 
	 * <p>Event routes themselves may also be added or removed dynamically.</p>  
	 * 
	 * <p>Routes will handle events of any kind. However, for the convenience of 
	 * not having to define numerous custom classes, the companion open-ended
	 * ApplicationEvent class is provided: it allows arbitrary keyed data 
	 * items to be attached to an event. But note that the Routes and 
	 * ApplicationEvent classes are completely independent of each other; 
	 * neither is aware of the existence of the other.</p> 
	 * 
	 * <p>Events may be dispatched by any UI Component (they must be set to 
	 * bubble) or centrally via <code>routes.dispatchEvent()</code>. 
	 * In all cases, the resolved handler will receive the dispatched event 
	 * as the only argument.</p>  
	 * 
	 * <p>To use routes, a Flex application should create a Routes instance and,
	 * ideally, make it available globally. Large multi-layered applications 
	 * may choose to employ multiple distinct Routes instances, where each 
	 * one may define a <em>communciation channel</em>.</p>   
	 * 
	 * <p>A component, to be the <em>target</em> of a route, needs to be 
	 * registered, typically with: 
	 * <code>routes.register(componentName, this)</code. 
	 * However, components do not need to be registered to dispatch events.
	 * </p>
	 * */
	public class Routes {
		
		// Internal Attributes 
		
		/**
		 * global container, that all application events bubble up to
		 * 
		 * this is also the value of currentTarget for all routed events
		 */
		private var _gContainer:EventDispatcher;
		
		/**
		 * the actual dispatcher object used by routes.dispatchEvent()
		 * 
		 * this is also the value of target for all events dispatched 
		 * in this way
		 */
		private var _gDispatcher:EventDispatcher;
		
		/**
		 * map of routes; a route specifies an (eventType, handler) connection,
		 * where the handler is itself an (objectName, methodName) pair:
		 * { 
		 *   eventType: [ (objectName, methodName), ... ],
		 *   ...
		 * }
		 * + alternative: { eventType: [
		 * 		{ instance:str, method:str, require:[[key, type, op, value]], 
		 * 		  conditions:[[name, value]] }
		 * ], ... } 
		 */
		private static var _map:Object = {};
		//private static var _remap:Object = {};
		
		// Constructor
		
		public function Routes(
			gDispatcher:EventDispatcher=null, // the global dispatcher
			gContainer:EventDispatcher=null // the global container
		) {
			_gDispatcher = gDispatcher;
			if (_gDispatcher==null) {
				// use the application as the global dispatcher
				_gDispatcher = Application(Application.application);
			}
			_gContainer = gContainer;
			if (_gContainer==null) {
				// use the application's topLevelSystemManager
				_gContainer = Application.application.systemManager.
					topLevelSystemManager;
			}
			trace("Routes._gDispatcher:", _gDispatcher);
			trace("Routes._gContainer:", _gContainer);
		}
		
		// Registry, of participating components
		
		/** 
		 * the dynamic container object to hold all registrations.
		 * */
		private var _registry:Object = {};
		
		/** 
		 * Register <code>value</code> under <code>key</code>. 
		 * */
		public function register(key:String, value:Object):void {
			_registry[key] = value; 
		}
		
		/** 
		 * Get the value registered under <code>key</code>.
		 * @return null if nothing had been previously stored for "key".
		 * */
		public function retrieve(key:String):Object {
			if (hasRegistration(key)) {
				return _registry[key];
			}
			return null;
		}
		
		/** 
		 * Unregister <code>key</code>.
		 * */
		public function unregister(key:String):void {
			if (hasRegistration(key)) {
				delete _registry[key];
			}
		}
		
		/** 
		 * Is <code>key</code> registered?
		 * */
		public function hasRegistration(key:String):Boolean {
			return _registry.hasOwnProperty(key);
		}
		
		/**
		 * Return a string listing of the registry. 
		 */
		public function listRegistrations():String {
			return ObjectUtil.toString(_registry);
		}

		// Registration of global event listeners
		
		/**
		 * Add the generic routing listener for this event type.
		 * If eventType starts with "/" then it is taken to be a RegExp.
 		 */
		private function _registerEvent(eventType:String, 
					useCapture:Boolean=false, priority:int=0, 
					useWeakReference:Boolean=false):void {
			if (!_gContainer.hasEventListener(eventType)) {
				_gContainer.addEventListener(eventType, routeEvent, 
					useCapture, priority, useWeakReference);
			}
		}
		
		/**
		 * Remove the generic routing listener for this event type
		 */
		private function _unregisterEvent(eventType:String):void {
			if (_gContainer.hasEventListener(eventType)) {
				_gContainer.removeEventListener(eventType, routeEvent);
			}
		}
				
		// Event Routes
		
		/**
		 * Process a (eventType, objectName, methodName) tuple to add
		 * (a) the generic routing listener for eventType and 
		 * (b) a route to own private routeMap for eventType. 
		 */
		public function addRoute(eventType:String, 
					registeredKey:String, callableName:String):void {
			trace("addRoute", eventType, registeredKey, callableName);
			_registerEvent(eventType);
			// add route to map
			if (!_map.hasOwnProperty(eventType)) {
				_map[eventType] = []; 
			}
			var added:Boolean = false;
			var om:Array; // [object, method]
			for each (om in _map[eventType]) {
				if ((om[0]==registeredKey)&&(om[1]==callableName)) {
					added = true;
					trace("    warning... route already in routeMap");
					break;
				}
			}
			if (!added) {
				_map[eventType].push([registeredKey, callableName]); 
			}
		}
		
		/**
		 * Remove a previously added route.
		 */
		public function removeRoute(eventType:String, 
					registeredKey:String, callableName:String):void {
			trace("removeRoute", eventType, registeredKey, callableName);
			var removed:Boolean = false;
			if (_map.hasOwnProperty(eventType)) {
				var objectMethods:Array = _map[eventType];
				var om:Array; // [object, method]
				for each (om in objectMethods) {
					if ((om[0]==registeredKey)&&(om[1]==callableName)) {
						objectMethods.splice(objectMethods.indexOf(om), 1);
						if (objectMethods.length==0) {
							_unregisterEvent(eventType);
						}
						removed = true;
						break;
					}
				}
			}
			if (!removed) {
				trace("    warning... route not found in routeMap");
			}
		}
		
		/**
		 * Check if the specified route is in the routeMap.
		 */
		public function hasRoute(eventType:String, 
					registeredKey:String, callableName:String):Boolean {
			if (_map.hasOwnProperty(eventType)) {
				var om:Array; // [object, method]
				for each (om in _map[eventType]) {
					if ((om[0]==registeredKey)&&(om[1]==callableName)) {
						return true;
					}
				}
			}
			return false;
		}
		
		/**
		 * Add a list of routes -- Convenience wrapper on addRoute(). 
		 */
		public function addRoutes(routes:Array):void {
			var route:Array;
			for each (route in routes) {
				addRoute(route[0], route[1], route[2]);
			} 
		}
		
		// Event Routing and Dispatching 
		
		/**
		 * Generic event listener as event routing mediator
		 */
		public function routeEvent(event:Event):void {
			trace("routeEvent", event);
			// add route to map
			if (!_map.hasOwnProperty(event.type)||(_map[event.type].length==0)) {
				trace("Route map has no entry defined for type:", event.type);
				return; 
			}
			var traceName:String;
			var pre1st:String = "routing to...";
			var pre2nd:String = "          ...";
			var om:Array; // [object, method]
			for each (om in _map[event.type]) {
				traceName = om[0]+ "." +om[1]+ "(event)";
				try {
					trace(pre1st, traceName);
					// resolve and invoke handler: object.method(event)
					retrieve(om[0])[om[1]](event);
				}
				catch (e:Error) {
					if (!retrieve(om[0])) {
						trace(pre2nd, "not registered:", om[0], " :");
					}
					else if (!retrieve(om[0]).hasOwnProperty(om[1])) {
						trace(pre2nd, "no such method:", traceName, " :");
					}
					else {
						trace(pre2nd, "FAILED:");
					}
					trace(e.getStackTrace());
				}
			}
			//+ for (re in _remap) { if (matches) { for each om ... } }
		}
		
		/**
		 * Central event dispatcher, may be used from anywhere within the 
		 * application, in particular by non-IEventDispatcher objects.  
		 */
		public function dispatchEvent(event:Event):Boolean {
			return _gDispatcher.dispatchEvent(event);
		}
		
	}
}